// Copyright 2016 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package cli

import (
	"context"
	"fmt"
	"html"
	"os"
	"sort"
	"strings"
	"time"

	"github.com/cockroachdb/cockroach/pkg/base"
	"github.com/cockroachdb/cockroach/pkg/build"
	"github.com/cockroachdb/cockroach/pkg/cli/clierrorplus"
	"github.com/cockroachdb/cockroach/pkg/cli/cliflagcfg"
	"github.com/cockroachdb/cockroach/pkg/cli/cliflags"
	"github.com/cockroachdb/cockroach/pkg/cli/clisqlexec"
	"github.com/cockroachdb/cockroach/pkg/security/username"
	"github.com/cockroachdb/cockroach/pkg/server"
	"github.com/cockroachdb/cockroach/pkg/server/serverpb"
	"github.com/cockroachdb/cockroach/pkg/settings"
	"github.com/cockroachdb/cockroach/pkg/settings/cluster"
	"github.com/cockroachdb/cockroach/pkg/testutils/serverutils"
	"github.com/cockroachdb/cockroach/pkg/ts/catalog"
	"github.com/cockroachdb/cockroach/pkg/upgrade/upgrades"
	"github.com/cockroachdb/errors"
	"github.com/cockroachdb/errors/oserror"
	slugify "github.com/mozillazg/go-slugify"
	"github.com/spf13/cobra"
	"github.com/spf13/cobra/doc"
)

var manPath string

var genManCmd = &cobra.Command{
	Use:   "man",
	Short: "generate man pages for CockroachDB",
	Long: `This command generates man pages for CockroachDB.

By default, this places man pages into the "man/man1" directory under the
current directory. Use "--path=PATH" to override the output directory. For
example, to install man pages globally on many Unix-like systems,
use "--path=/usr/local/share/man/man1".
`,
	Args: cobra.NoArgs,
	RunE: clierrorplus.MaybeDecorateError(runGenManCmd),
}

func runGenManCmd(cmd *cobra.Command, args []string) error {
	info := build.GetInfo()
	header := &doc.GenManHeader{
		Section: "1",
		Manual:  "CockroachDB Manual",
		Source:  fmt.Sprintf("CockroachDB %s", info.Tag),
	}

	if !strings.HasSuffix(manPath, string(os.PathSeparator)) {
		manPath += string(os.PathSeparator)
	}

	if _, err := os.Stat(manPath); err != nil {
		if oserror.IsNotExist(err) {
			if err := os.MkdirAll(manPath, 0755); err != nil {
				return err
			}
		} else {
			return err
		}
	}

	if err := doc.GenManTree(cmd.Root(), header, manPath); err != nil {
		return err
	}

	// TODO(cdo): The man page generated by the cobra package doesn't include a list of commands, so
	// one has to notice the "See Also" section at the bottom of the page to know which commands
	// are supported. I'd like to make this better somehow.

	fmt.Println("Generated CockroachDB man pages in", manPath)
	return nil
}

var autoCompletePath string

var genAutocompleteCmd = &cobra.Command{
	Use:   "autocomplete [shell]",
	Short: "generate autocompletion script for CockroachDB",
	Long: `Generate autocompletion script for CockroachDB.

If no arguments are passed, or if 'bash' is passed, a bash completion file is
written to ./cockroach.bash. If 'fish' is passed, a fish completion file
is written to ./cockroach.fish. If 'zsh' is passed, a zsh completion file is written
to ./_cockroach. Use "--out=/path/to/file" to override the output file location.

Note that for the generated file to work on OS X with bash, you'll need to install
Homebrew's bash-completion package (or an equivalent) and follow the post-install
instructions.
`,
	Args:      cobra.OnlyValidArgs,
	ValidArgs: []string{"bash", "zsh", "fish"},
	RunE:      clierrorplus.MaybeDecorateError(runGenAutocompleteCmd),
}

func runGenAutocompleteCmd(cmd *cobra.Command, args []string) error {
	var shell string
	if len(args) > 0 {
		shell = args[0]
	} else {
		shell = "bash"
	}

	var err error
	switch shell {
	case "bash":
		if autoCompletePath == "" {
			autoCompletePath = "cockroach.bash"
		}
		err = cmd.Root().GenBashCompletionFile(autoCompletePath)
	case "fish":
		if autoCompletePath == "" {
			autoCompletePath = "cockroach.fish"
		}
		err = cmd.Root().GenFishCompletionFile(autoCompletePath, true /* include description */)
	case "zsh":
		if autoCompletePath == "" {
			autoCompletePath = "_cockroach"
		}
		err = cmd.Root().GenZshCompletionFile(autoCompletePath)
	}
	if err != nil {
		return err
	}

	fmt.Printf("Generated %s completion file: %s\n", shell, autoCompletePath)
	return nil
}

var includeAllSettings bool
var excludeSystemSettings bool
var showSettingClass bool
var classHeaderLabel string
var classLabels []string

var genSettingsListCmd = &cobra.Command{
	Use:   "settings-list",
	Short: "output a list of available cluster settings",
	Long: `
Output the list of cluster settings known to this binary.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		wrapCode := func(s string) string {
			if sqlExecCtx.TableDisplayFormat == clisqlexec.TableDisplayRawHTML {
				return fmt.Sprintf("<code>%s</code>", s)
			}
			return s
		}

		wrapDivSlug := func(key settings.InternalKey, name settings.SettingName) string {
			toDisplay := string(name)
			if sqlExecCtx.TableDisplayFormat == clisqlexec.TableDisplayRawHTML {
				if string(key) != string(name) {
					toDisplay = fmt.Sprintf("%s<br />(alias: %s)", name, key)
				}
				return fmt.Sprintf(`<div id="setting-%s" class="anchored">%s</div>`, slugify.Slugify(string(key)), wrapCode(toDisplay))
			}
			if string(key) != string(name) {
				toDisplay = fmt.Sprintf("%s (alias: %s)", name, key)
			}
			return toDisplay
		}

		// Fill a Values struct with the defaults.
		s := cluster.MakeTestingClusterSettings()
		settings.NewUpdater(&s.SV).ResetRemaining(context.Background())

		var rows [][]string
		for _, key := range settings.Keys(settings.ForSystemTenant) {
			setting, ok := settings.LookupForLocalAccessByKey(key, settings.ForSystemTenant)
			if !ok {
				panic(fmt.Sprintf("could not find setting %q", key))
			}
			name := setting.Name()

			if excludeSystemSettings && setting.Class() == settings.SystemOnly {
				continue
			}

			if !includeAllSettings && setting.Visibility() != settings.Public {
				continue
			}

			typ, ok := settings.ReadableTypes[setting.Typ()]
			if !ok {
				panic(fmt.Sprintf("unknown setting type %q", setting.Typ()))
			}
			var defaultVal string
			if sm, ok := setting.(*settings.VersionSetting); ok {
				defaultVal = sm.SettingsListDefault()
			} else {
				defaultVal = setting.String(&s.SV)
				if override, ok := upgrades.SettingsDefaultOverrides[key]; ok {
					defaultVal = override
				}
			}

			settingDesc := setting.Description()
			alterRoleLink := "ALTER ROLE... SET: https://www.cockroachlabs.com/docs/stable/alter-role.html"
			if sqlExecCtx.TableDisplayFormat == clisqlexec.TableDisplayRawHTML {
				settingDesc = html.EscapeString(settingDesc)
				alterRoleLink = `<a href="alter-role.html"><code>ALTER ROLE... SET</code></a>`
			}
			if strings.Contains(string(name), "sql.defaults") || strings.Contains(string(key), "sql.defaults") {
				settingDesc = fmt.Sprintf(`%s
This cluster setting is being kept to preserve backwards-compatibility.
This session variable default should now be configured using %s`,
					settingDesc,
					alterRoleLink,
				)
			}

			row := []string{wrapDivSlug(key, name), typ, wrapCode(defaultVal), settingDesc}
			if showSettingClass {
				class := "unknown"
				switch setting.Class() {
				case settings.SystemOnly:
					class = classLabels[0]
				case settings.SystemVisible:
					class = classLabels[1]
				case settings.ApplicationLevel:
					class = classLabels[2]
				}
				row = append(row, class)
			}
			if includeAllSettings {
				if setting.Visibility() == settings.Public {
					row = append(row, "public")
				} else {
					row = append(row, "reserved")
				}
			}
			rows = append(rows, row)
		}

		cols := []string{"Setting", "Type", "Default", "Description"}
		align := "dddd"
		if showSettingClass {
			cols = append(cols, classHeaderLabel)
			align += "d"
		}
		if includeAllSettings {
			cols = append(cols, "Visibility")
			align += "d"
		}
		sliceIter := clisqlexec.NewRowSliceIter(rows, align)
		return sqlExecCtx.PrintQueryOutput(os.Stdout, stderr, cols, sliceIter)
	},
}

var genMetricListCmd = &cobra.Command{
	Use:   "metric-list",
	Short: "output a list of available metrics",
	Long: `
Output the list of metrics typical for a node.
`,
	RunE: func(cmd *cobra.Command, args []string) error {
		ctx := context.Background()

		// Use a testserver. We presume that all relevant metrics exist in
		// test servers too.
		sArgs := base.TestServerArgs{
			Insecure:          true,
			DefaultTestTenant: base.ExternalTestTenantAlwaysEnabled,
		}
		s, err := server.TestServerFactory.New(sArgs)
		if err != nil {
			return err
		}
		srv := s.(serverutils.TestServerInterfaceRaw)
		defer srv.Stopper().Stop(ctx)

		// We want to only return after the server is ready.
		readyCh := make(chan struct{})
		srv.SetReadyFn(func(bool) {
			close(readyCh)
		})

		// Start the server.
		if err := srv.Start(ctx); err != nil {
			return err
		}

		// Wait until the server is ready to action.
		select {
		case <-readyCh:
		case <-time.After(5 * time.Second):
			return errors.AssertionFailedf("could not initialize server in time")
		}

		var sections []catalog.ChartSection

		// Retrieve the chart catalog (metric list) for the system tenant over RPC.
		retrieve := func(layer serverutils.ApplicationLayerInterface, predicate func(catalog.MetricLayer) bool) error {
			conn, err := layer.RPCClientConnE(username.RootUserName())
			if err != nil {
				return err
			}
			client := serverpb.NewAdminClient(conn)

			resp, err := client.ChartCatalog(ctx, &serverpb.ChartCatalogRequest{})
			if err != nil {
				return err
			}
			for _, section := range resp.Catalog {
				if !predicate(section.MetricLayer) {
					continue
				}
				sections = append(sections, section)
			}
			return nil
		}

		if err := retrieve(srv, func(layer catalog.MetricLayer) bool {
			return layer != catalog.MetricLayer_APPLICATION
		}); err != nil {
			return err
		}
		if err := retrieve(srv.TestTenant(), func(layer catalog.MetricLayer) bool {
			return layer == catalog.MetricLayer_APPLICATION
		}); err != nil {
			return err
		}

		// Sort by layer then metric name.
		sort.Slice(sections, func(i, j int) bool {
			return sections[i].MetricLayer < sections[j].MetricLayer ||
				(sections[i].MetricLayer == sections[j].MetricLayer &&
					sections[i].Title < sections[j].Title)
		})

		// Populate the resulting table.
		cols := []string{"Layer", "Metric", "Description", "Y-Axis Label", "Type", "Unit", "Aggregation", "Derivative"}
		var rows [][]string
		for _, section := range sections {
			rows = append(rows,
				[]string{
					section.MetricLayer.String(),
					section.Title,
					section.Charts[0].Metrics[0].Help,
					section.Charts[0].AxisLabel,
					section.Charts[0].Metrics[0].MetricType.String(),
					section.Charts[0].Units.String(),
					section.Charts[0].Aggregator.String(),
					section.Charts[0].Derivative.String(),
				})
		}
		align := "dddddddd"
		sliceIter := clisqlexec.NewRowSliceIter(rows, align)
		return sqlExecCtx.PrintQueryOutput(os.Stdout, stderr, cols, sliceIter)
	},
}

// GenCmd is the root of all gen commands. Exported to allow modification by CCL code.
var GenCmd = &cobra.Command{
	Use:   "gen [command]",
	Short: "generate auxiliary files",
	Long:  "Generate manpages, example shell settings, example databases, etc.",
	RunE:  UsageAndErr,
}

var genCmds = []*cobra.Command{
	genManCmd,
	genAutocompleteCmd,
	genExamplesCmd,
	genHAProxyCmd,
	genSettingsListCmd,
	genMetricListCmd,
}

func init() {
	genManCmd.PersistentFlags().StringVar(&manPath, "path", "man/man1",
		"path where man pages will be outputted")
	genAutocompleteCmd.PersistentFlags().StringVar(&autoCompletePath, "out", "",
		"path to generated autocomplete file")
	genHAProxyCmd.PersistentFlags().StringVar(&haProxyPath, "out", "haproxy.cfg",
		"path to generated haproxy configuration file")
	cliflagcfg.VarFlag(genHAProxyCmd.Flags(), &haProxyLocality, cliflags.Locality)

	f := genSettingsListCmd.PersistentFlags()
	f.BoolVar(&includeAllSettings, "all-settings", false,
		"include undocumented 'internal' settings")
	f.BoolVar(&excludeSystemSettings, "without-system-only", false,
		"do not list settings only applicable to system tenant")
	f.BoolVar(&showSettingClass, "show-class", false,
		"show the setting class")
	f.StringVar(&classHeaderLabel, "class-header-label", "Class",
		"label to use in the output for the class column")
	f.StringSliceVar(&classLabels, "class-labels",
		[]string{"system-only", "system-visible", "application"},
		"label to use in the output for the various setting classes")

	GenCmd.AddCommand(genCmds...)
}
